谈 C 和 Lisp 调试

#Technomous #Debug #Lisp

C 语言是一种静态类型、编译型、命令式的语言,它需要先编译成可执行文件,然后运行。而 Lisp 是一种动态类型、解释型、函数式的语言,它不需要编译,直接在解释器中运行。C 和 Lisp 的调试方式有:

前两种是 C 和 Lisp 都支持的调试方式,后两种是 Lisp 特有的调试方式。C 的主要调试方式一般是在 debug 模式下编译程序,设置断点,单步执行或者逐过程执行,观察变量的值、函数的调用过程等。Lisp 语言的调试方式一般是 REPL(read-eval-print loop)中输入表达式,观察输出结果或者错误信息。产生这种差异的原因在于大多数非 Lisp 的编译型语言分离了编译时和运行时。即每次你看程序的运行效果时,你必须先编译,然后才能启动程序、执行编译后的机器码/字节码,产生效果。这里的编译时和运行时是严格分离的两个步骤,这类编程语言里,你无法办到:

唯独 Lisp 语言同时拥有以上两个特性,它可以在编译时执行自身程序代码,也可以在运行时编译并执行代码。因此,用 Lisp 开发程序就像面对活体,你在 REPL 里发送过去的表达式进行求值,就像是给这个活体添加不同的肢体组织。意思是,可以你在 Lisp 程序运行时,不断添加、修改、替换该运行程序的功能,而不用像其他语言,每修改一行代码都得重新编译然后运行程序才能看到运行效果。而且好些语言里项目配置不当或者模块分离不够,修改一行代码编译的时间常常以分钟或半小时计。

换句话说,通过 REPL,你完全可以对你这个处于运行时的程序进行全方位的自省。什么意思?当程序运行时,我可以进入程序内完全不同的 namespace(命名空间),求值检查当前的运行状态,当我发现里面某个函数有问题时,我可以立刻写一个新的修复后的函数替换掉原先的函数定义,此时,还在运行的程序,立刻就变成了修复后的运行程序。这就是为何当年 NASA 的火箭程序用 Lisp 写,你总不能说让远在太空的火箭控制程序停止下,等我本地编译好修复后的 C/C++ 程序,然后远程发送过去再部署运行。就算远程部署没问题,远程传一个不小的二进制程序,可能通信都成问题。而且最重要的,有些 bug 只能在生产环境下复现,就像火箭程序,你本地可没什么完美的模拟环境,你也根本没办法或很难远程 debug 正在运行的 C/C++ 程序(现在虽然工具链成熟,比如 Visual Studio,你可以启动 debug 模式,程序运行时会加载好链接源码的符号,但就算 debug 成功,你也只能重新编译重启程序,而且这都是本地的工具链技术,可不是什么远程通信指令)。

写火箭程序真的不常见,但通过这个例子也能体会到 Lisp REPL 带来的明显好处,它可以不用借助任何其他工具链,不用不断编译重启测试程序,可以更快速的进行函数库功能探索,极速的反馈、自省,带来的开发时巨大的信心提升和心流的持续稳定。